package mini.compiler;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;

import mini.Proc;
import mini.Program;
import mini.util.MiniUtil;
import mini.util.SymbolicNames;

/**
 * This class is responsible for taking unlinked symbolic RAL code and generating
 * two things: linked RAL code and an initial memory load.
 * 
 * For an example of what the input file would look like, refer to the slides on
 * the compiler (https://www.cs.drexel.edu/~jjohnson/2011-12/spring/cs550/lectures/compiler.pdf)
 * and in particular, slide 20.
 * 
 * For example of what linked output would look like, refer to the same slides
 * and in particular, slide 23
 * 
 * For example of what the memory file would look like, refer to the example in
 * resources/sample_mem.txt (which accompanies resources/sample_program.txt), in
 * tandem with the same slide set, and slide 22 in particular.
 * 
 * @author Jennifer
 *
 */
public class Linker {
	
	public Linker() {
	}
	
	/**
	 * Don't worry about opening or closing the readers/writers, that's done elsewhere.
	 * 
	 * @param hm The hash map of the symbol table. This lists all the constants, variables,
	 * and temp variables that should have space somewhere in the memory. The keys are
	 * identifiers used in the symbolic code for loads and stores. If the corresponding value
	 * is null, it does not have to be initialized (you can set it to 0). If it does have a
	 * non-null value, that is what the memory cell needs to be prepopulated. (The non-null
	 * values are in the case of constants.)
	 * @param functiontable A bunch of functions
	 * @param inputReader A reader that points to a newline-delimited set of symbolic
	 * RAL instructions, ala slide 20
	 * @param memoryWriter Should look something like resources/sample_mem.txt
	 * @param programWriter Should look something like resources/sample_program.txt
	 */
	public void link( HashMap<String,Integer> hm, HashMap<String,Proc> functiontable,
			BufferedReader inputReader, Writer memoryWriter, Writer programWriter ) {
		
		// First we set up the memory map, writing it out to a file as it does. 
		// Since EVERYTHING is a function now, the only variables that we store
		// in the global memory are the constants. Everything else gets read
		// indirectly off an activation record
		HashMap<String, Integer> memoryMap = generateMemoryMap( hm, memoryWriter );
		
		// Next, we need to write the linked RAL instructions
		writeLinkedProgram( functiontable, memoryMap, inputReader, programWriter );
		
	}

	/**
	 * This takes a symbolic RAL program (the inputReader) and a memory map
	 * (the memoryMap), and translates it to a linked RAL program without
	 * symbolic values.
	 * 
	 * If there is a problem reading or writing, the program will
	 * exit abnormally with an error.
	 * 
	 * @param functiontable The functiontable lists all the functions
	 * @param memoryMap This maps from variable names to memory locations.
	 * The linked version of the code will, amongst other things, change
	 * every symbolic reference to an absolute reference by using this map
	 * @param inputReader This is the RAL program that has symbolic links.
	 * It will be formatted like on slide 20
	 * @param programWriter This is where to write the RAL program with
	 * absolute links. It should look like the sample_program.txt file
	 * in resources, or what's in slide 23
	 */
	private void writeLinkedProgram(HashMap<String,Proc> functiontable, HashMap<String, Integer> memoryMap,
			BufferedReader inputReader, Writer programWriter) {
		
		// As we look for symbolic locations, keep the program in memory
		// as an ordered array of strings
		List<String> theProgram = new ArrayList<String>();
		
		// First thing to do is figure out any symbolic locations. These are
		// marked in the code by a line that starts with "<string>: <code> <operand>".
		// They will be needed later for jump calls
		HashMap<String,Integer> locationMap = new HashMap<String,Integer>();
		try {
			int lineCounter = 1;	// Remember, we are 1-based!
			String line = inputReader.readLine();
			
			while( line != null ) {
				// Record the line read
				theProgram.add(line.trim());
				
				// See if there is a "<Location>:" denoted on this line
				String[] markers = MiniUtil.getMarkersFromLine(line);
				if( markers != null ) {
					// And if so, record it to use later in the event of JUMP instructions
					for( String loc : markers ) {
						locationMap.put( loc, new Integer(lineCounter) );
					}
				}
				
				// Advance to next line
				line = inputReader.readLine();
				
				// Only advance the line counter if it's NOT a comment
				if( !MiniUtil.isComment(line) ) {
					lineCounter++;
				}
			}
		} catch (IOException e) {
			System.err.println( "Unable to read symbolic program file" );
			e.printStackTrace();
			System.exit(1);
		}
		
		// The location map now maps location labels to the line number they represent
		// we need to augment it so that it also maps function names to their line number
		for( String funcid : functiontable.keySet() ) {
			Proc proc = functiontable.get(funcid);
			if( proc != null ) {
				Integer lineNumber = locationMap.get(proc.getLocationLabel());
				if( lineNumber != null ) {
					locationMap.put( funcid, lineNumber );
				}
			}
		}
		
		// Now we can translate the symbolic program to one with
		// absolute locations. We use the location map when we
		// encounter jumps, and the memoryMap for symbolic variables
		translateToAbsoluteProgram( theProgram, memoryMap, locationMap, programWriter );
	}

	/**
	 * Takes a RAL program and translates it from its symbolic form, to its
	 * absolute form. There are three things that this involves:
	 * 
	 * <ol>
	 * <LI> Stripping off location markers on lines of code. That is, something
	 *      that looks like 'L1: LDA T1' should be changed to 'LDA T1', and
	 *      anything like 'LA: L2: LDA T1' should be changed to 'LDA T1'
	 * <LI> Replacing symbolic memory addresses with absolute memory addresses.
	 *      That is, something that looks like 'LDA T1' should be changed to
	 *      'LDA 15' (or whatever, based on the memory map)
	 * <LI> Replacing symbolic location addresses (after a jump style statement)
	 *      to absolute addresses. So something that looks like 'JMP L1' would
	 *      be translated to 'JMP 37' (or whatever, based on the locationMap)
	 * </LI>
	 * 
	 * Note that any comment lines are preserved.
	 * 
	 * @param theProgram An array of program lines, in order
	 * @param memoryMap Maps variable names to absolute memory addresses
	 * @param locationMap Maps location names to absolute program space
	 * addresses
	 * @param programWriter Where the output goes
	 */
	private void translateToAbsoluteProgram(List<String> theProgram,
			HashMap<String, Integer> memoryMap,
			HashMap<String, Integer> locationMap, Writer programWriter) {
		
		try {
			int totalLineCounter = 0;	// This one includes counting comments and blank lines, so we can use it for error reporting
			for( String line : theProgram ) {
				
				totalLineCounter++;
				
				// Ignore comments, do not print
				if( !MiniUtil.isComment(line) ) {

					// First, strip off any comments and save for later
					String commentlessLine = MiniUtil.removeAnyComments(line);
					int commentStartAt = line.indexOf(";");
					String commentStr = null;
					if( commentStartAt != -1 ) {
						commentStr = line.substring(commentStartAt+1);
					}
					
					// Now, strip off any markers 
					String strippedLine = MiniUtil.removeAnyLocationMarkers(commentlessLine);
					
					// Parse out the operator, the operand
					StringTokenizer strtok = new StringTokenizer(strippedLine);
					String instruction = null;
					String operand = null;
					if( strtok.hasMoreTokens() ) {
						instruction = strtok.nextToken();
					}
					
					// Actually, some instructions don't have an operand....
					if( !CodeGenerator.needsOperand(instruction) ) {
						// We still don't want it to be null, though
						operand = "";
					} else if( strtok.hasMoreTokens() ) {
						operand = strtok.nextToken();
					}
					
					// Hopefully we have them!
					if( instruction == null || operand == null ) {
						MiniUtil.reportParseError(totalLineCounter, line, "linking");
						programWriter.write( "; Error linking code: " + line + Program.NEWLINE );
					} else {
						
						// Now, we have a location field that is an int. This corresponds to
						// an absolute representation of the operand's symbolic value. If it's
						// a halt, well, it's nothing. If there is a lookup error, it'll stay
						// -1. Otherwise, if it's a jump instruction we do a lookup in the
						// locationMap, and for a variable, we look it up in the memoryMap.
						Integer location = null;
						if( CodeGenerator.isJumpInstruction(instruction) ) {
							location = locationMap.get(operand);
							// If we didn't find it, there's a problem! Report and press on.
							if( location == null ) {
								MiniUtil.reportParseError(totalLineCounter, line, "linking");
								programWriter.write( "; Error linking code: " + line + Program.NEWLINE );
							}
						} else if( CodeGenerator.needsOperand(instruction) ) {
							location = memoryMap.get(operand);
							// If we didn't find it, there's a problem! Report and press on.
							if( location == null ) {
								MiniUtil.reportParseError(totalLineCounter, line, "linking");
								programWriter.write( "; Error linking code: " + line + Program.NEWLINE );
							}
						}
						
						// Finally! We can print everything out
						programWriter.write( instruction );
						programWriter.write( " " );
						if( CodeGenerator.needsOperand(instruction)) {
							if( location == null ) {
								programWriter.write("<error>");
							} else {
								programWriter.write( location.toString() );
							}
							programWriter.write( " " );
						}
						if( commentStr != null ) {
							programWriter.write(commentStr);
						}
						programWriter.write( Program.NEWLINE );
					}
				}
			}
		} catch( IOException e ) {
			System.err.println( "Unable to write the memory map file" );
			e.printStackTrace();
			System.exit(1);
		}
	}

	/**
	 * This takes a symbolic table, representing variables (constants, temp,
	 * and user) and values to initialize them with, and creates the appropriate
	 * memory file for the RAL Machine to read in.
	 *
	 * @param hm A map of variables names (the keys) to values (the values). 
	 * Constant begin with the string 'Constant' and should have a non-null
	 * value. Temp variables begin with the word 'temp' and have a null
	 * value. All others are user variables
	 * @param memoryWriter This is where the memory map gets written, in a 
	 * RAL Machine compatible format. See sample_mem.txt for an example of
	 * the format. ; denotes comments. There need to be two white-space
	 * delimited columns of number. The first is the memory address space,
	 * base 1. Make sure these are in order. The second is the initial value.
	 * For non-constants it can be 0. For constants, it should be its value.
	 * A comment with the name of the variable is optional but desired for
	 * each row.
	 * @return A memory map that uses a variable name as the key, with the
	 * non-null integer values being the memory address location. The size
	 * of this should be the same size as the input hash map (and also the
	 * number of non-comment lines output to the memory writer)
	 */
	private HashMap<String, Integer> generateMemoryMap(
			HashMap<String, Integer> hm, Writer memoryWriter) {
		
		// None of this should happen
		if( hm == null || memoryWriter == null ) {
			System.err.println( "Internal error writing the memory map file; unexpected null value" );
			System.exit(1);
		}
		
		// We'll build up this mapping
		HashMap<String,Integer> memoryMap = new HashMap<String,Integer>();
		
		// SP and FP should be initialized properly
		int initialFramePointer = getInitialFramePointer(hm);
		memoryMap.put( SymbolicNames.getSymbolicNameForSP(), SymbolicNames.getMemoryLocForSP() );
		formatMemoryRow( memoryWriter, SymbolicNames.getSymbolicNameForSP(), initialFramePointer, SymbolicNames.getMemoryLocForSP() );
		memoryMap.put( SymbolicNames.getSymbolicNameForFP(), SymbolicNames.getMemoryLocForFP() );
		formatMemoryRow( memoryWriter, SymbolicNames.getSymbolicNameForFP(), initialFramePointer, SymbolicNames.getMemoryLocForFP() );
		
		// While the FPB and Scratch can be initialized to 0
		memoryMap.put( SymbolicNames.getSymbolicNameForFPB(), SymbolicNames.getMemoryLocForFPB() );
		formatMemoryRow( memoryWriter, SymbolicNames.getSymbolicNameForFPB(), 0, SymbolicNames.getMemoryLocForFPB() );
		memoryMap.put( SymbolicNames.getSymbolicNameForScratch(), SymbolicNames.getMemoryLocForScratch() );
		formatMemoryRow( memoryWriter, SymbolicNames.getSymbolicNameForScratch(), 0, SymbolicNames.getMemoryLocForScratch() );
		
		// Keep track of where in memory we are writing
		int counter = SymbolicNames.getMemoryLocAfterPointers() ;
		
		// We ONLY write out the constants
		for( String varName : hm.keySet() ) {
			Integer value = hm.get(varName);
			// If it's an unassigned constant
			if( isConstant(varName,value) && memoryMap.get(varName) == null ) {
				// Store it in the memory map
				memoryMap.put(varName, new Integer(counter) );
				
				// And write it to the output writer
				formatMemoryRow( memoryWriter, "Constant: " + varName, value, counter++ );
			}
		}
		
		return memoryMap;
	}

	/**
	 * Compute what the initial frame pointer should be. Remember
	 * there's one space reserved in memory for every constant.
	 * 
	 * @param hm Symbol table, to include constants
	 * @return The initial frame pointer
	 */
	private int getInitialFramePointer(HashMap<String, Integer> hm) {

		int pointer = 0;
		
		for( String var : hm.keySet() ) {
			if( isConstant(var,hm.get(var)) ) {
				pointer++;
			}
		}
		
		// But we have to pretend like our initial SP/FP are one
		// behind. This is because the first thing we do when creating
		// a NEW FP is add 1 to the old SP. By setting it one less
		// than the next empty space in memory, the first time we
		// create an AR, its FP will be the first empty space
		return pointer-1;
	}

	/**
	 * Output a row to the memory writer of the form:
	 * 
	 * <memoryLoc> <value> ; <varName>
	 * 
	 * If value is null, use a 0 instead. Follow with a newline.
	 * 
	 * Any IO errors will abort further processing.
	 * 
	 * @param memoryWriter Where to send it
	 * @param varName Name of the variable
	 * @param value Its initial value (if null, initialize to 0)
	 * @param memoryLoc The location in memory to set it for
	 */
	private void formatMemoryRow(Writer memoryWriter, String varName,
			Integer value, int memoryLoc) {
		
		// None of this should happen
		if( memoryWriter == null || varName == null ) {
			System.err.println( "Internal error writing the memory map file; unexpected null value" );
			System.exit(1);
		}
		
		// Build up what to write
		StringBuffer strbuf = new StringBuffer();
		strbuf.append( memoryLoc );
		strbuf.append( " " );
		strbuf.append( (value == null ? 0 : value ) );
		strbuf.append( " ; " );
		strbuf.append( varName );
		strbuf.append( Program.NEWLINE );
		
		// And write it
		try {
			memoryWriter.write( strbuf.toString() );
		} catch (IOException e) {
			System.err.println( "Unable to write the memory map file" );
			e.printStackTrace();
			System.exit(1);
		}
	}

	/**
	 * Given a name and a value, determine if it's a 
	 * constant. Though constants typically start with
	 * a variable name of 'Constant', a safer check is
	 * that it has a non-null value.
	 * @param varName Variable name. Constants typically
	 * start with 'Constant'
	 * @param value Its integer values. All non-null values
	 * indicate constants.
	 * @return True if it's a constant, false otherwise
	 */
	private boolean isConstant(String varName, Integer value) {
		return value != null;
	}

	/**
	 * Given a name and value, determine if it's a temp
	 * variable. Currently temp variables are determined
	 * by having a null value, and starting with the
	 * word 'temp'. (Temp variable names are generated
	 * by a method in the Expr.java class.)
	 * 
	 * @param varName A name. If it's a temp variable it
	 * begins with 'temp'
	 * @param value A value If it's a temp variable, it's
	 * null
	 * @return True if it's a temp variable, false otherwise
	 */
	private boolean isTempVariable(String varName, Integer value) {
		return (value == null) && (varName != null && SymbolicNames.isTempVariable(varName) );
	}

	/**
	 * Given a name and value, determine if it's a user
	 * variable. Something is a user variable if it's not a
	 * constant and it's not a temp.
	 * 
	 * @param varName A name.
	 * @param value A value
	 * @return True if it's a user variable, false otherwise
	 */
	private boolean isUserVariable(String varName, Integer value) {
		return !isConstant(varName,value) && !isTempVariable(varName,value);
	}

}
